from typing import Tuple, Optional, List, Any, Dict
import time
from decimal import Decimal
import re
from datetime import datetime
from datetime import date
import urllib.parse
import requests
import json

from sqlalchemy.orm.session import Session
from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import StaleElementReferenceException
from selenium.common.exceptions import NoSuchElementException

from .browser import Browser
from .utils import ShippingData
from utils import wait
from db.utils import (
    get_engine_and_session_of_db,
    close_engine_and_session_of_db,
    filter_new_shipments,
    add_list_of_shipments
)
import settings


class SeleniumScraper:
    """ Инструменты для парсинга связанные с Selenium """

    def scroll_the_page(self, browser, a=.2, b=.5, w=0, h=50):
        """
        Прокрутить страницу js
        w - прокрутить страницу по ширине
        h - прокрутить страницу по высоте
        """

        browser.execute_script(f"window.scrollBy({w}, {h})")
        wait(a, b)


    def scroll_page_with_delay(self, browser: Chrome) -> None:
        """ Пролистываем всю страницу с задержкой """

        scroll_to_bottom_script: str = """
        let finish = undefined;
        const boo = setInterval(foo, 200);

        function foo() {
          if (window.scrollY < document.body.offsetHeight - 1000) {
            window.scrollBy(0, 100);
          } else {
            finish = true;
            clearInterval(boo);
          };
        };
        """
        browser.execute_script(scroll_to_bottom_script)

        finished_script: str = 'return window.scrollY > document.body.offsetHeight - 1000;'

        while True:
            if browser.execute_script(finished_script): break
            wait(.9, 1.1)

    def move_and_click_on_element(
        self,
        browser: Chrome,
        xpath: Optional[str] = None,
        element: Optional[WebElement] = None
    ) -> None:
        """ Перемещаемся к элементу и кликаем по нему """

        if xpath is None and element is None:
            message: str = 'Не было передано xpath и element'
            raise ValueError(message)

        while True:
            if xpath:
                element = self.get_page_element_wait(browser, xpath)

            try:
                actions = ActionChains(browser)
                actions.move_to_element(element)
                actions.perform()
                self.scroll_the_page(browser, h=500)
                wait(.3, .5)
                element.click()
                wait(.2, .4)
                break
            except StaleElementReferenceException:
                pass

    def move_and_click_on_elements(self, browser: Chrome, xpath: str) -> None:
        """ Перемещаемся к элементам и кликаем по ним """

        list_of_elements: List[WebElement] = browser.find_elements(
            By.XPATH,
            xpath
        )
        for element in list_of_elements:
            self.move_and_click_on_element(browser, element=element)

    def get_page_element_wait(
        self,
        browser: Chrome,
        xpath: str
    ) -> WebElement:
        """ Получаем элемент старницы ожидая его появления """

        while True:
            try:
                element = browser.find_element(By.XPATH, xpath)
                return element
            except NoSuchElementException as error:
                message = f'Элемент по {xpath} не был найден, ждем...'
                print(message)
                wait(.5, .6)


    def get_elements_from_page(self, browser: Chrome, xpath: str) -> List[WebElement]:
        """ Получаем список элементов со страницы """

        list_of_elements: List[WebElement] = browser.find_elements(
            By.XPATH,
            xpath
        )
        return list_of_elements


    def wait_for_element(
        self,
        browser: Chrome,
        xpath: str,
        waiting_time: int
    ) -> Optional[WebElement]:
        """ Ждем появление элемента на странице """

        count: int = 0

        while True:
            if count > waiting_time: break
            try:
                element: WebElement = browser.find_element(By.XPATH, xpath)
                return element
            except NoSuchElementException as error:
                message = f'Ожидаем появление элемента {xpath}...'
                print(message)
                wait(.9, 1.1)
                count += 1

    def enter_text_in_element(
        self,
        browser: Chrome,
        xpath: str,
        text: str
    ) -> None:
        """ Вводим текст в элемент """

        element: WebElement = self.get_page_element_wait(browser, xpath)

        for symbol in text:
            element.send_keys(symbol)
            wait(.2, .3)


    def click_on_element(self, browser: Chrome, xpath: str) -> None:
        """ Кликаем по элементу """

        element: WebElement = self.get_page_element_wait(browser, xpath)
        element.click()
        wait(.4, .5)


    def open_page(
        self,
        link: str,
        browser: Chrome,
        xpath: str,
        waiting_time: int
    ) -> None:
        """ Открываем какую-то страницу дожидаясь появления какого-то элемента """

        browser.get(link)

        while True:
            element: Optional[WebElement] = \
                self.wait_for_element(browser, xpath, waiting_time)
            if element: break
            browser.refresh()


class Scraper(Browser, SeleniumScraper):
    """ Общие инструменты для парсинга """
    pass


class AuthorizationScraper(Scraper):
    """ Инструменты для парсинга связанные с авторизацией на сайте """

    def __init__(self):
        self.AUTHORIZATION_LINK = 'https://www.centraldispatch.com/protected'

    def enter_login(self, browser: Chrome, login: str) -> None:
        """ Вводим логин пользователя """
        self.enter_text_in_element(
            browser,
            '//input[@id="Username"]',
            login
        )

    def enter_password(self, browser: Chrome, password: str) -> None:
        """ Вводим пароль пользователя """
        self.enter_text_in_element(
            browser,
            '//input[@id="password"]',
            password
        )

    def enter_required_data(
        self,
        browser: Chrome,
        account: Tuple[str, str]
    ) -> None:
        """ Вводим необходимые данные для авторизации """

        self.enter_login(browser, account.login)
        self.enter_password(browser, account.password)

    def click_on_authorization_button(self, browser: Chrome) -> None:
        """ Нажимаем на кнопку авторизации """
        self.click_on_element(
            browser,
            '//button[@id="loginButton"]'
        )

    def perform_authorization(
        self,
        browser: Chrome,
        account: Tuple[str, str]
    ) -> None:
        """ Выполняем авторизацию на сайте """

        self.open_page(
            self.AUTHORIZATION_LINK,
            browser,
            '//input[@id="Username"]',
            7
        )
        self.enter_required_data(browser, account)
        self.click_on_authorization_button(browser)


class PopUpWindowsScraper(Scraper):
    """ Инструменты связанные со всплывающими окнами """

    def check_pop_up_window_one(self, browser: Chrome) -> None:
        """ Проверяем наличие первого всплывающего окна """

        element: Optional[WebElement] = self.wait_for_element(
            browser,
            '//*[contains(text(), "We Need Your Feedback")]',
            5
        )
        if not element: return

        self.click_on_element(
            browser,
            '//*[contains(text(), "We Need Your Feedback")]/parent::h3/../..//button[contains(text(), "Dismiss"]',
        )

    def check_for_pop_up_windows(self, browser: Chrome) -> None:
        """ Проверяем наличие всплывающих окон """

        self.check_pop_up_window_one(browser)


class ShipmentsScraper(Scraper):
    """ Инструменты связанные с парсингом перевозок """

    def get_list_of_prices(self, browser: Chrome) -> Any:
        """ Получаем список цен перевозок """

        elements: List[WebElement] = browser.find_elements(
            By.XPATH,
            '//*[@title="Add to Worklist"]/../following-sibling::div'
        )
        for element in elements:
            print(element.text, '\n\n')


    def open_shipping_page(self, browser: Chrome, link_for_parsing: str) -> Any:
        """ Открываем страницу с перевозками """

        browser.get(link_for_parsing)

        while True:
            result: Optional[WebElement] = self.wait_for_element(
                browser,
                link_for_parsing,
                15
            )
            if result: break
            browser.refresh()

    def open_all_shipments(self, browser: Chrome) -> None:
        """ Открываем все перевозки """

        self.move_and_click_on_elements(
            browser,
            '//*[@title="Add to Worklist"]/../../..'
        )


    def get_shipment_data(
        self,
        browser: Chrome,
        shipment_number: int
    ) -> ShippingData:
        """ Получаем данные перевозки """

        def get_price() -> Decimal:
            """ Получаем цену """

            nonlocal shipping_element
            price: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[@data-pc]'

            ).text
            price: str = re.sub('[^\d\,]', '', price)
            price: float = Decimal(re.sub(',', '.', price))
            return price

        def get_number_of_miles() -> int:
            """ Получаем количество миль """

            nonlocal shipping_element
            number_of_miles: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[@data-pc]/following-sibling::div'

            ).text
            try:
                number_of_miles: int = int(number_of_miles.split(' mi ')[0])
                return number_of_miles
            except ValueError:
                return 0

        def get_price_per_mile() -> float:
            """ Получаем цену за милю """

            nonlocal shipping_element
            number_of_miles: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[@data-pc]/following-sibling::div'
            ).text
            try:
                number_of_miles: str = number_of_miles.split(' mi ')[1]
                number_of_miles: float = float(re.sub('[^\d\.]', '', number_of_miles))
                return number_of_miles
            except IndexError:
                return 0

        def get_car_information() -> str:
            """ Получаем информацию о машине """

            nonlocal shipping_element
            car_information: str = self.get_page_element_wait(
                browser,
                shipping_element + '/div[2]//span[contains(text(), "Vehicle Info")]/../following-sibling::div'
            ).text
            return car_information

        def get_pick_up_location() -> str:
            """ Получаем место принятия """

            nonlocal shipping_element
            pick_up_location: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Pick-Up Location")]/following-sibling::*[self::span or self::a][1]'
            ).text
            return pick_up_location

        def get_delivery_location() -> str:
            """ Получаем место доставки """

            nonlocal shipping_element
            element: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Delivery Location")]/following-sibling::a[1]'
            )
            delivery_location_text: str = element.text
            delivery_location_link: str = element.get_attribute('href')
            delivery_location: str = f'{delivery_location_text}\n\n\n{delivery_location_link}'
            return delivery_location

        def get_pick_up_or_after_date() -> datetime:
            """ Получаем дату встречи """

            nonlocal shipping_element
            pick_up_or_after_date: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Pick-Up on or After Date")]/following-sibling::div[1]'
            ).text
            pick_up_or_after_date: datetime = \
                datetime.strptime(pick_up_or_after_date, '%m/%d/%y')

            return pick_up_or_after_date

        def get_desired_delivery_date() -> datetime:
            """ Получаем желаемую дату доставки """

            nonlocal shipping_element
            desired_delivery_date: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Desired Delivery Date")]/following-sibling::div[1]'
            ).text

            if desired_delivery_date == 'Unspecified':
                desired_delivery_date: datetime = \
                    datetime(day=1, month=1, year=1999)
            else:
                desired_delivery_date: datetime = \
                    datetime.strptime(desired_delivery_date, '%m/%d/%y')

            return desired_delivery_date

        def get_posted_date() -> datetime:
            """ Получаем дату публикации """

            nonlocal shipping_element
            posted_date: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Posted Date")]/following-sibling::div[1]'
            ).text
            posted_date: datetime = \
                datetime.strptime(posted_date, '%m/%d/%y')

            return posted_date

        def get_company_name() -> str:
            """ Получаем название компании """

            nonlocal shipping_element
            company_name: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Company")]/following-sibling::a'
            ).text
            return company_name

        def get_phone_number() -> str:
            """ Получаем номер телефона """

            nonlocal shipping_element
            phone_number: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last()]'
            ).text
            return phone_number

        def get_hours() -> str:
            """ Получаем часы работы """

            nonlocal shipping_element
            hours: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last() -1]'
            ).text
            return hours

        def get_rating_in_old_system_percent() -> float:
            """ Получаем рейтинг по старой системе в виде процента """

            nonlocal shipping_element
            rating_in_old_system_percent: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last() -2]'
            ).text

            rating_in_old_system_percent: float = \
                float(re.sub('[^\d\.%]', '', rating_in_old_system_percent).split('%')[0])

            return rating_in_old_system_percent

        def get_rating_in_old_system_overall() -> int:
            """ Получаем общий рейтинг по старой системе """

            nonlocal shipping_element
            rating_in_old_system_overall: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last() -2]'
            ).text
            rating_in_old_system_overall: str = \
                re.findall('\(\d+?\)', rating_in_old_system_overall.split('|')[0])[0]

            rating_in_old_system_overall: int = \
                int(re.sub('[^\d]', '', rating_in_old_system_overall))

            return rating_in_old_system_overall

        def get_rating() -> float:
            """ Получаем рейтинг """

            nonlocal shipping_element
            rating: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last() -2]'
            ).text

            try:
                rating: float = float(rating.split('|')[1].split('/')[0])
                return rating
            except ValueError:
                return -1.0

        def get_overall_rating() -> int:
            """ Получаем общий рейтинг """

            nonlocal shipping_element
            overall_rating: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[text()="Company"]/../div[last() -2]'
            ).text

            try:
                overall_rating: str = \
                    re.findall('\(\d+?\)', overall_rating.split('|')[1])[0]

                overall_rating: int = int(re.sub('[^\d]', '', overall_rating))
                return overall_rating
            except IndexError:
                return -1

        def get_additional_info() -> str:
            """ Получаем дополнительную информацию """

            nonlocal shipping_element
            additional_info: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Additional Info")]/following-sibling::div'
            ).text
            return additional_info

        def get_order_id() -> str:
            """ Получаем id заказа """

            nonlocal shipping_element
            order_id: str = self.get_page_element_wait(
                browser,
                shipping_element + '//div[contains(text(), "Order ID")]/following-sibling::div'
            ).text
            return order_id


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        shipping_element: str = f'(//*[text()="GENERAL INFO"])[{shipment_number}]/ancestor::div[@data-listing-id]'
        price: Decimal = get_price()
        number_of_miles: int = get_number_of_miles()
        price_per_mile: float = get_price_per_mile()
        car_information: str = get_car_information()
        pick_up_location: str = get_pick_up_location()
        delivery_location: str = get_delivery_location()
        pick_up_or_after_date: datetime = get_pick_up_or_after_date()
        desired_delivery_date: datetime = get_desired_delivery_date()
        posted_date: datetime  = get_posted_date()
        company_name: str = get_company_name()
        phone_number: str = get_phone_number()
        hours: str = get_hours()
        rating_in_old_system_percent: float = get_rating_in_old_system_percent()
        rating_in_old_system_overall: int = get_rating_in_old_system_overall()
        rating: float = get_rating()
        overall_rating: int = get_overall_rating()
        additional_info: str = get_additional_info()
        order_id: str = get_order_id()

        shipping_data: ShippingData = ShippingData(
            order_id=order_id,
            price=price,
            number_of_miles=number_of_miles,
            price_per_mile=price_per_mile,
            car_information=car_information,
            pick_up_location=pick_up_location,
            delivery_location=delivery_location,
            pick_up_or_after_date=pick_up_or_after_date,
            desired_delivery_date=desired_delivery_date,
            posted_date=posted_date,
            company_name=company_name,
            phone_number=phone_number,
            hours=hours,
            rating_in_old_system_percent=rating_in_old_system_percent,
            rating_in_old_system_overall=rating_in_old_system_overall,
            rating=rating,
            overall_rating=overall_rating,
            additional_info=additional_info
        )
        return shipping_data


    def get_shipments_from_page(self, browser: Chrome) -> List[ShippingData]:
        """ Получаем перевозки со страницы """

        def open_shipment() -> None:
            """ Открываем перевозку """

            nonlocal browser
            nonlocal shipment_number

            if shipment_number == 1:
                self.move_and_click_on_element(
                    browser,
                    '//*[@title="Add to Worklist"]/../../..'
                )


            self.move_and_click_on_element(
                browser,
                xpath='(//*[text()="GENERAL INFO"])[last()]/ancestor::div[@data-listing-id]/following-sibling::div[1]'
            )

        def select_25_elements() -> None:
            """ Выбираем 25 элементов для отображения если установлено не оно """

            element: WebElement = self.get_page_element_wait(
                browser,
                '//div[@id="per-page-select"]'
            )
            element_content: str = element.text
            if element_content == '25': return

            element.click()

            element: WebElement = self.get_page_element_wait(
                browser,
                '//ul[contains(@class, "cd-search-MuiMenu-list")]/li[1]'
            ).click()

            wait(.7, 1)

        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        list_of_shipments: List[ShippingData] = []
        select_25_elements()
        shipment_number = 1

        while True:
            if shipment_number == 25: break

            open_shipment()
            shipping_data: ShippingData = \
                self.get_shipment_data(browser, shipment_number)
            list_of_shipments.append(shipping_data)

            shipment_number += 1

        return list_of_shipments


    def get_new_shipments_from_page(
        self,
        session: Session,
        browser: Chrome,
        link_for_parsing: str
    ) -> List[ShippingData]:
        """ Получаем новые перевозки со страницы """

        self.open_page(
            link_for_parsing,
            browser,
            '//*[@title="Add to Worklist"]/ancestor::div[@data-listing-id]//div[@data-pc]',
            15
        )
        shipments_from_page: List[ShippingData] = \
            self.get_shipments_from_page(browser)
        list_of_new_shipments: List[ShippingData] = \
            filter_new_shipments(session, shipments_from_page)

        return list_of_new_shipments


class ShipmentsScraperRequests(Scraper):
    """ Инструменты связанные с парсингом перевозок с помощью библиотеки requests """

    def open_shipping_page(self, browser: Chrome, link_for_parsing: str) -> None:
        """ Открываем страницу с перевозками """

        self.open_page(
            link_for_parsing,
            browser,
            '//*[@title="Add to Worklist"]/ancestor::div[@data-listing-id]//div[@data-pc]',
            15
        )

        while True:
            if browser.current_url == link_for_parsing: break
            self.open_page(
                link_for_parsing,
                browser,
                '//*[@title="Add to Worklist"]/ancestor::div[@data-listing-id]//div[@data-pc]',
                15
            )
        time.sleep(5)


    def get_token_for_request(self, list_of_logs: List[Dict]) -> str:
        """ Получаем токен для запроса """

        necessary_link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        for element_str in list_of_logs:
            if not necessary_link in str(element_str): continue

            element = json.loads(element_str['message'])

            try:
                element['message']['params']['request']['headers']['Authorization']
            except KeyError:
                continue

            if element['message']['params']['request']['url'] == necessary_link:
                token: str = element['message']['params']['request']['headers']['Authorization']
                return token


    def get_filters_for_request(
        self,
        list_of_logs: List[Dict]
    ) -> Dict:
        """ Получаем фильтры для запроса """

        list_of_filters: List[Dict] = []
        necessary_link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        for element_str in list_of_logs:
            if not necessary_link in str(element_str): continue

            element: Dict = json.loads(element_str['message'])

            try:
                element['message']['params']['request']['postData']
            except KeyError:
                continue

            if element['message']['params']['request']['url'] == necessary_link:
                filters_str: str = \
                    element['message']['params']['request']['postData']
                filters: Dict = json.loads(filters_str)
                list_of_filters.append(filters)

        return list_of_filters[-1]


    def get_shipping_list(self, token: str, filters_for_request: Dict) -> Dict:
        """ Получаем список перевозок """

        link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        headers: Dict[str, str] = {
            'accept': 'application/json',
            'authorization': token,
            'content-type': 'application/json'
        }
        response = requests.post(
            link,
            headers=headers,
            data=json.dumps(filters_for_request)
        )
        shipping_list: Dict = response.json()
        return shipping_list


    def convert_shipping_list(self, shipping_list: Dict) -> List[ShippingData]:
        """ Преобразуем список перевозок в нужный формат """

        def get_order_id() -> str:
            """ Получаем id перевозки """

            nonlocal shipping
            return str(shipping['id'])

        def get_price() -> Decimal:
            """ Получаем цену """

            nonlocal shipping
            price: Decimal = Decimal(shipping['price']['total'])
            return price

        def get_number_of_miles() -> int:
            """ Получаем количество миль """

            nonlocal shipping
            return shipping['distance']

        def add_car_information(dictionary) -> None:

            nonlocal car_information

            for key in dictionary:
                if type(dictionary[key]) == dict:
                    add_car_information(dictionary[key])
                else:
                    if not dictionary[key] or key == 'id': continue
                    car_information += f'{key}: {dictionary[key]}\n'

        def get_car_information() -> None:
            """ Получаем информацию о машине """

            nonlocal shipping

            for dictionary in shipping['vehicles']:
                add_car_information(dictionary) 

        def get_pick_up_location() -> str:

            nonlocal shipping
            state: str = shipping['origin']['state']
            city: str = shipping['origin']['city']
            zip: str = shipping['origin']['zip']

            pick_up_location: str = f'{state}: {city}, {zip}'
            return pick_up_location

        def get_delivery_location() -> str:

            nonlocal shipping
            state: str = shipping['destination']['state']
            city: str = shipping['destination']['city']
            zip: str = shipping['destination']['zip']
            pick_up_location: str = f'{state}: {city}, {zip}'
            longitude: float = shipping['destination']['geoCode']['longitude']
            latitude: float = shipping['destination']['geoCode']['latitude']
            link: str = f'https://www.google.com/maps?q={latitude},{longitude}&ll={latitude},{longitude}&z=10'
            pick_up_location: str = f'{pick_up_location}\n\n\n{link}'
            return pick_up_location

        def get_pick_up_or_after_date() -> datetime:

            nonlocal shipping
            pick_up_or_after_date: str = shipping['availableDate']
            pick_up_or_after_date: str = pick_up_or_after_date.split('T')[0]
            pick_up_or_after_date: datetime = datetime.strptime(
                pick_up_or_after_date,
                '%Y-%m-%d'
            )
            return pick_up_or_after_date

        def get_company_name() -> str:
            """ Получаем название компании """

            nonlocal shipping
            company_name: str = shipping['shipper']['companyName']
            return company_name

        def get_phone_number() -> str:
            """ Получаем телефон """

            nonlocal shipping
            phone: str = shipping['shipper']['phone']
            return phone

        def get_hours() -> str:
            """ Получаем часы работы """

            nonlocal shipping
            hours: str = shipping['shipper']['hoursOfOperation']
            return hours

        def get_rating_in_old_system_percent() -> float:
            """ Получаем рейтинг по старой системе в проценте """

            nonlocal shipping
            rating_in_old_system_percent: float = shipping['shipper']['rating']
            return rating_in_old_system_percent

        def get_rating_in_old_system_overall() -> int:
            """ Получаем общий рейтинг по старой системе """

            nonlocal shipping
            rating_in_old_system_overall: int = shipping['shipper']['numberOfRatings']
            return rating_in_old_system_overall

        def get_rating() -> float:
            """ Получаем рейтинг """

            nonlocal shipping
            rating: int = shipping['shipper']['overallRating']['averageRating']
            return rating

        def get_overall_rating() -> int:
            """ Получаем общий рейтинг """

            nonlocal shipping
            overall_rating: int = shipping['shipper']['overallRating']['totalAmount']
            return overall_rating

        def get_additional_info() -> str:
            """ Получаем дополнительную информацию """

            nonlocal shipping
            additional_info: int = shipping['additionalInfo']
            if additional_info:
                return additional_info
            else:
                return 'Unspecified'

        def get_price_per_mile() -> float:
            """ Получаем цену за одну милю """

            nonlocal price
            nonlocal number_of_miles

            if not number_of_miles or number_of_miles < 0: return 0.0
            price_per_mile: float = price / number_of_miles
            price_per_mile: Decimal = round(price_per_mile, 2)
            return price_per_mile

        def get_posted_date() -> datetime:

            nonlocal shipping
            created_date: str = shipping['createdDate']
            created_date: str = created_date.split('T')[0]
            created_date: datetime = datetime.strptime(
                created_date,
                '%Y-%m-%d'
            )
            return created_date


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        list_of_shipments: List[ShippingData] = []
        shipping_list: List[Dict] = shipping_list['items']

        for shipping in shipping_list:

            order_id: str = get_order_id()
            price: Decimal = get_price()
            number_of_miles: int = get_number_of_miles()
            price_per_mile = get_price_per_mile()
            car_information: str = ''
            get_car_information()
            pick_up_location: str = get_pick_up_location()
            delivery_location: str = get_delivery_location()
            pick_up_or_after_date: datetime = get_pick_up_or_after_date()
            posted_date: datetime = get_posted_date()
            company_name: str = get_company_name()
            phone_number: str = get_phone_number()
            hours: str = get_hours()
            rating_in_old_system_percent: float = get_rating_in_old_system_percent()
            rating_in_old_system_overall: int = get_rating_in_old_system_overall()
            rating: float = get_rating()
            overall_rating: int = get_overall_rating()
            additional_info: str = get_additional_info()
            desired_delivery_date = datetime(year=1999, month=1, day=1)

            shipping_data: ShippingData = ShippingData(
                order_id=order_id,
                price=price,
                number_of_miles=number_of_miles,
                price_per_mile=price_per_mile,
                car_information=car_information,
                pick_up_location=pick_up_location,
                delivery_location=delivery_location,
                pick_up_or_after_date=pick_up_or_after_date,
                desired_delivery_date=desired_delivery_date,
                posted_date=posted_date,
                company_name=company_name,
                phone_number=phone_number,
                hours=hours,
                rating_in_old_system_percent=rating_in_old_system_percent,
                rating_in_old_system_overall=rating_in_old_system_overall,
                rating=rating,
                overall_rating=overall_rating,
                additional_info=additional_info
            )
            list_of_shipments.append(shipping_data)

        return list_of_shipments


    def get_new_shipments_from_page(
        self,
        session: Session,
        browser: Chrome,
        link_for_parsing: str
    ) -> List[ShippingData]:
        """ Получаем новые перевозки со страницы """

        self.open_shipping_page(browser, link_for_parsing)
        list_of_logs: List[Dict] = browser.get_log('performance')

        token: str = self.get_token_for_request(list_of_logs)
        filters_for_request: Dict = self.get_filters_for_request(
            list_of_logs
        )
        shipping_list: Dict = self.get_shipping_list(token, filters_for_request)
        converted_shipping_list: List[ShippingData] = \
            self.convert_shipping_list(shipping_list)
        list_of_new_shipments: List[ShippingData] = \
            filter_new_shipments(session, converted_shipping_list)

        return list_of_new_shipments


class TrackShipmentsScraper(
    AuthorizationScraper,
    PopUpWindowsScraper,
    ShipmentsScraperRequests
):
    """ Инструменты для парсинга связанные с отслеживанием перевозок """

    def send_shipments_to_telegram(self, list_of_shipments: List[ShippingData]) -> None:
        """ Отправляем перевозки в телеграм """

        def get_message(shipping: ShippingData) -> str:
            """ Получаем сообщение для отправки в телеграм """

            delivery_location, delivery_location_link = \
                shipping['delivery_location'].split('\n\n\n')

            if shipping['desired_delivery_date'] == datetime(year=1999, day=1, month=1):
                desired_delivery_date = 'Unspecified'
            else:
                desired_delivery_date = \
                    shipping['desired_delivery_date'].strftime('%m/%d/%y')

            pick_up_or_after_date: str = \
                shipping['pick_up_or_after_date'].strftime('%m/%d/%y')

            posted_date: str = \
                shipping['posted_date'].strftime('%m/%d/%y')

            message: str = '1. ID - {}\n2. Цена, количество миль, цена за милю - ${}, {}mi, ${}/mi\n3. Информация о машине:\n{}\n4. Pick-Up Location - {}\n5. Delivery Location - {} {}\n6. Pick-Up on or After Date - {}\n7. Desired Delivery Date - {}\n8. Posted Date - {}\n9. Company - {}\n10. Phone/hours - {}/{}\n11. Rating in Old System - {}% ({})\n12. Overall Rating - {}/5 ({})\n13. Additional Info - {}' \
                .format(
                    shipping['order_id'],
                    shipping['price'],
                    shipping['number_of_miles'],
                    shipping['price_per_mile'],
                    shipping['car_information'],
                    shipping['pick_up_location'],
                    delivery_location,
                    delivery_location_link,
                    pick_up_or_after_date,
                    desired_delivery_date,
                    posted_date,
                    shipping['company_name'],
                    shipping['phone_number'],
                    shipping['hours'],
                    shipping['rating_in_old_system_percent'],
                    shipping['rating_in_old_system_overall'],
                    shipping['rating'],
                    shipping['overall_rating'],
                    shipping['additional_info']
                )
            encoded_message = urllib.parse.quote(message.encode('utf-8'))
            return encoded_message


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        part_of_request: str = 'https://api.telegram.org/bot{}/sendMessage?chat_id={}' \
            .format(
                settings.TELEGRAM_BOT_TOKEN,
                settings.TELEGRAM_CHAT_ID
            )

        for shipping in list_of_shipments:
            message: str = get_message(shipping)
            request = f'{part_of_request}&text={message}'
            requests.get(request)


    def start(self, link_for_parsing: str, account: Tuple[str, str]) -> None:
        """ Начинаем отслеживать перевозки """

        engine, session = get_engine_and_session_of_db()
        browser: Chrome = self.get_browser(True)

        self.perform_authorization(browser, account)

        list_of_new_shipments: List[ShippingData] = \
            self.get_new_shipments_from_page(session, browser, link_for_parsing)

        add_list_of_shipments(session, list_of_new_shipments)
        self.send_shipments_to_telegram(list_of_new_shipments)

        self.close_browser(browser)
        close_engine_and_session_of_db(engine, session)
